package paint;


import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;


    /**
     * Parses an image file, and stores each object in a queue.
     * @author Dustin Striplin
     *
     */
    public class Display {
    	private static final int SHAPE_INDEX = 0;
    	private static final int RED_INDEX = 1;
    	private static final int GREEN_INDEX = 2;
    	private static final int BLUE_INDEX = 3;
    	
    	private static final int THETA = 1;
    	private static final int X = 1;
    	private static final int Y = 2;
    	private static final int Z = 3;
    	
    	private static final int EYE_X = 1;
    	private static final int EYE_Y = 2;
    	private static final int EYE_Z = 3;
    	private static final int AT_X = 4;
    	private static final int AT_Y = 5;
    	private static final int AT_Z = 6;
    	private static final int UP_X = 7;
    	private static final int UP_Y = 8;
    	private static final int UP_Z = 9;
    	
    	private static final int LEFT = 1;
    	private static final int RIGHT = 2;
    	private static final int BOTTOM = 3;
    	private static final int TOP = 4;
    	private static final int ZNEAR = 5;
    	private static final int ZFAR = 6;
    	
    	private static final String TRI = "TRI";
    	private static final String LINE = "LINE";
    	private static final String RGB = "RGB";
    	private static final String LOAD_IDENTITY_MATRIX = "LOAD_IDENTITY_MATRIX";
    	private static final String TRANSLATE = "TRANSLATE";
    	private static final String ROTATEX = "ROTATEX";
    	private static final String ROTATEY = "ROTATEY";
    	private static final String ROTATEZ = "ROTATEZ";
    	private static final String LOOKAT = "LOOKAT";
    	private static final String FRUSTUM = "FRUSTUM";
    	private static final String ORTHO = "ORTHO";
    	private static final String SCALE = "SCALE";
    	private static final String WIREFRAME_CUBE = "WIREFRAME_CUBE";
    	private static final String SOLID_CUBE = "SOLID_CUBE"; 	
    	
    	private int mWidth;
    	private int mHeight;
    	//private Point3D mMidPoint;//the midPoint of all the objects
       
        private Matrix ctm;
        private Matrix perspective;
        private Matrix lookat;
        private Matrix projection;
        
        
        public static final int FRUSTUM_MODE = -1;
    	public static final int ORTHO_MODE = -2;        
        
    	public int perspectiveMode = ORTHO_MODE;
    	
        public double left = .5;
        public double right = .5;
        public double bottom = .5;
        public double top = .5;
        public double zNear = .5;
        public double zFar = .5;
    	
    	public ArrayList<WorldObject> mObjects;
    	
    	public Display(File imageFile) {
            perspective = Matrix.identityMatrix();
            perspectiveMode = ORTHO_MODE;
            lookat = Matrix.identityMatrix();
            projection = Matrix.identityMatrix();
            ctm = Matrix.identityMatrix();
    		try {
				parseFile(imageFile);
			} catch (IOException e) {
				System.out.println("Error reading file");
			}

    	}
		private void parseFile(File imageFile) throws IOException {
			BufferedReader br = new BufferedReader(new FileReader(imageFile));
			String[] dims = br.readLine().split(" ");//get the dims
			//don't use index 0, this contains "DIM"
			mWidth = Integer.parseInt(dims[1]);
			mHeight = Integer.parseInt(dims[2]);
			mObjects = new ArrayList<WorldObject>();
			
			//get each object and store it in the queue
			String nextLine = br.readLine();
			String[] colors = nextLine.split(" ");//get color 
			while(nextLine != null) {
				if(!nextLine.equals("")){
					String[] brokenLine = nextLine.split("\\s+");//split up by whitespace
					String command = brokenLine[0];
					if(command.equals(RGB)) {
						colors = brokenLine;
					} else if(command.equals(LOAD_IDENTITY_MATRIX)) {
						ctm = Matrix.identityMatrix();
					} else if(command.equals(TRANSLATE)) {
						ctm = Matrix.translate(ctm, Double.parseDouble(brokenLine[X]), 
								Double.parseDouble(brokenLine[Y]), Double.parseDouble(brokenLine[Z]));
					} else if(command.equals(ROTATEX)) {
						ctm = Matrix.rotateX(ctm, Double.parseDouble(brokenLine[THETA]));
					} else if(command.equals(ROTATEY)) {
						ctm = Matrix.rotateY(ctm, Double.parseDouble(brokenLine[THETA]));
					} else if(command.equals(ROTATEZ)) {
						ctm = Matrix.rotateZ(ctm, Double.parseDouble(brokenLine[THETA]));
					} else if(command.equals(SCALE)) {
						ctm = Matrix.scale(ctm, Double.parseDouble(brokenLine[X]), 
								Double.parseDouble(brokenLine[Y]), Double.parseDouble(brokenLine[Z]));
					} else if(command.equals(LOOKAT)) {
						lookat(brokenLine);
					} else if(command.equals(FRUSTUM)) {
						frustum(brokenLine);
					} else if(command.equals(ORTHO)) {
						ortho(brokenLine);
					} else {
						mObjects.add(generateObject(colors, brokenLine));
					}
				}
				nextLine = br.readLine();
			}
			br.close();
		}
		
		/**
		 * Generates a DisplayObject from the given color code & object specs
		 * @param colors the mixture of color intensities for this object
		 * @param objectSpecs A String[] where the first element is the object type, and the rest are coordinates
		 * @return A DisplayObject containing the passed in information
		 */
		private WorldObject generateObject(String[] colors, String[] objectSpecs) {
			double r = Double.parseDouble(colors[RED_INDEX]);
			double g = Double.parseDouble(colors[GREEN_INDEX]);
			double b = Double.parseDouble(colors[BLUE_INDEX]);
			ArrayList<Vector4D> vectors = new ArrayList<Vector4D>();
			if(objectSpecs[SHAPE_INDEX].equals(TRI) ||
					objectSpecs[SHAPE_INDEX].equals(LINE)) {
				//generate a 2d object by setting z value to 0
				for(int i = 1; i < objectSpecs.length; i+=3) {
					Vector4D vect = new Vector4D(generate3DVector(objectSpecs[i], objectSpecs[i+1], objectSpecs[i+2]));
					vectors.add(vect);
				}
			} else if(objectSpecs[0].equals(WIREFRAME_CUBE)) {
				vectors.addAll(generateWireframeCube());
			} else if(objectSpecs[0].equals(SOLID_CUBE)) {
				vectors.addAll(generateSolidCube());
			}
			WorldObject obj = new WorldObject(r, g, b, vectors);
			performTransformation(obj, ctm); //perform the desired transformations
			return obj; //add object to queue
		}
		
		private List<Vector4D> generateWireframeCube() {
			ArrayList<Vector4D> cube = new ArrayList<Vector4D>();
			cube.add(new Vector4D(.5,.5,.5));
			cube.add(new Vector4D(-.5,.5,.5));
			cube.add(new Vector4D(-.5,-.5,.5));
			cube.add(new Vector4D(.5,-.5,.5));
			cube.add(new Vector4D(.5,-.5,-.5));
			cube.add(new Vector4D(.5,.5,-.5));
			cube.add(new Vector4D(-.5,.5,-.5));
			cube.add(new Vector4D(-.5,-.5,-.5));
			return cube;
		}
		
		private List<Vector4D> generateSolidCube() {
			ArrayList<Vector4D> cube = new ArrayList<Vector4D>();
			//TRI .5 .5 .5  -.5 .5 .5  -.5 -.5 .5
			cube.add(new Vector4D(.5, .5, .5));
			cube.add(new Vector4D(-.5, .5, .5));
			cube.add(new Vector4D(-.5, -.5, .5));
			
			//TRI -.5 -.5 .5  .5 -.5 .5  .5 .5 .5
			cube.add(new Vector4D(-.5, -.5, .5));
			cube.add(new Vector4D(.5, -.5, .5));
			cube.add(new Vector4D(.5, .5, .5));
			
			//TRI .5 .5 -.5  .5 .5 .5  .5 -.5 .5
			cube.add(new Vector4D(.5, .5, -.5));
			cube.add(new Vector4D(.5, .5, .5));
			cube.add(new Vector4D(.5, -.5, .5));
			
			//TRI .5 -.5 .5  .5 -.5 -.5 .5 .5 -.5
			cube.add(new Vector4D(.5, -.5, .5));
			cube.add(new Vector4D(.5, -.5, -.5));
			cube.add(new Vector4D(.5, .5, -.5));
			
			//TRI .5 .5 .5  .5 .5 -.5  -.5 .5 .5
			cube.add(new Vector4D(.5, .5, .5));
			cube.add(new Vector4D(.5, .5, -.5));
			cube.add(new Vector4D(-.5, .5, .5));
			
			//TRI -.5 .5 .5  .5 .5 -.5  -.5 .5 -.5
			cube.add(new Vector4D(-.5, .5, .5));
			cube.add(new Vector4D(.5, .5, -.5));
			cube.add(new Vector4D(-.5, .5, -.5));
			
			//TRI -.5 -.5 -.5  .5 -.5 -.5  .5 .5 -.5
			cube.add(new Vector4D(-.5, -.5, -.5));
			cube.add(new Vector4D(.5, -.5, -.5));
			cube.add(new Vector4D(.5, .5, -.5));
			
			//TRI .5 .5 -.5  -.5 .5 -.5  -.5 -.5 -.5
			cube.add(new Vector4D(.5, .5, -.5));
			cube.add(new Vector4D(-.5, .5, -.5));
			cube.add(new Vector4D(-.5, -.5, -.5));
			
			//TRI -.5 -.5 .5  -.5 -.5 -.5  -.5 .5 -.5
			cube.add(new Vector4D(-.5, -.5, .5));
			cube.add(new Vector4D(-.5, -.5, -.5));
			cube.add(new Vector4D(-.5, .5, -.5));
			
			//TRI -.5 .5 -.5  -.5 .5 .5  -.5 -.5 .5
			cube.add(new Vector4D(-.5, .5, -.5));
			cube.add(new Vector4D(-.5, .5, .5));
			cube.add(new Vector4D(-.5, -.5, .5));
			
			//TRI -.5 -.5 -.5  -.5 -.5 .5  .5 -.5 -.5
			cube.add(new Vector4D(-.5, -.5, -.5));
			cube.add(new Vector4D(-.5, -.5, .5));
			cube.add(new Vector4D(.5, -.5, -.5));
			
			//TRI .5 -.5 -.5  -.5 -.5 .5  .5 -.5 .5
			cube.add(new Vector4D(.5, -.5, -.5));
			cube.add(new Vector4D(-.5, -.5, .5));
			cube.add(new Vector4D(.5, -.5, .5));
			
			return cube;
		}
		
		private Vector3D generate3DVector(String x, String y, String z) {
			return new Vector3D(Double.parseDouble(x), Double.parseDouble(y), Double.parseDouble(z));
		}
		
	    private void performTransformation(WorldObject obj, Matrix tm) {
	    	for(Vector4D vect: obj.mPoints){
	    		Vector4D temp = Matrix.multiply(tm, vect);
	    		vect.setX(temp.getX());
	    		vect.setY(temp.getY());
	    		vect.setZ(temp.getZ());
	    	}		
	    }
	    
		private void lookat(String[] brokenLine) {
			if(brokenLine.length != 10) {
				return;
			}
			
			double eyeX = Double.parseDouble(brokenLine[EYE_X]);
			double eyeY = Double.parseDouble(brokenLine[EYE_Y]);
			double eyeZ = Double.parseDouble(brokenLine[EYE_Z]);
			double atX = Double.parseDouble(brokenLine[AT_X]);
			double atY = Double.parseDouble(brokenLine[AT_Y]);
			double atZ = Double.parseDouble(brokenLine[AT_Z]);
			double upX = Double.parseDouble(brokenLine[UP_X]);
			double upY = Double.parseDouble(brokenLine[UP_Y]);
			double upZ = Double.parseDouble(brokenLine[UP_Z]);
			lookat = Matrix.lookAt(eyeX, eyeY, eyeZ, atX, atY, atZ, upX, upY, upZ);		
			projection = Matrix.multiplyMatrices(lookat, perspective);
		}
		
		private void frustum(String[] brokenLine) {
			if(brokenLine.length != 7) {
				return;
			}

			setBoundaries(brokenLine);
			setFrustum();
		}
		
		
		private void ortho(String[] brokenLine) {
			if(brokenLine.length != 7) {
				return;
			}
			setBoundaries(brokenLine);
			setOrtho();
		}
		
		private void setBoundaries(String[] brokenLine) {
			left = Double.parseDouble(brokenLine[LEFT]);
			right = Double.parseDouble(brokenLine[RIGHT]);
			bottom = Double.parseDouble(brokenLine[BOTTOM]);
			top = Double.parseDouble(brokenLine[TOP]);
			zNear = Double.parseDouble(brokenLine[ZNEAR]);
			zFar = Double.parseDouble(brokenLine[ZFAR]);
		}
		
		public void setFrustum() {
			perspectiveMode = FRUSTUM_MODE;
			recalculatePerspective();
		}
		
		public void setOrtho() {
			perspectiveMode = ORTHO_MODE;
			recalculatePerspective();
		}
		
		public int getWidth() {
			return mWidth;
		}
		
		public int getHeight() {
			return mHeight;
		}
		
		public Matrix getCTM() {
			return ctm;
		}
		
		public void setCTM(Matrix transformation) {
			ctm = transformation;
		}
		
	    public WorldObject project(WorldObject obj) {
	    	List<Vector4D> points = new ArrayList<Vector4D>();
	    	for(Vector4D vect: obj.mPoints){
	    		Vector4D temp = Matrix.multiply(projection, vect); //this removes the z value
	    		points.add(temp);
	    	}
	    	return new WorldObject(obj.r, obj.g, obj.b, points);
	    }
		
		private void rotateWorld(Matrix tm) {
	    	for(WorldObject obj: mObjects) {
	    		performTransformation(obj, tm);
	    	}
		}
		
	    public void rotateX(double degrees) {
	    	rotateWorld(Matrix.rotateX(Matrix.identityMatrix(), degrees));
	    }
	    
	    public void rotateY(double degrees) {
	    	rotateWorld(Matrix.rotateY(Matrix.identityMatrix(), degrees));
	    }
	    
	    public void rotateZ(double degrees) {
	    	rotateWorld(Matrix.rotateZ(Matrix.identityMatrix(), degrees));
	    }
	    
	    /**
	     * create a new projection matrix from the lookat and perspective projections
	     */
	    public void recalculatePerspective() {
	    	if(perspectiveMode == FRUSTUM_MODE) {
				perspective = Matrix.frustum(left, right, bottom, top, zNear, zFar);
	    	} else  {
				perspective = Matrix.ortho(left, right, bottom, top, zNear, zFar);
	    	}
			projection = Matrix.multiplyMatrices(lookat, perspective);
	    }
		

	    

		
		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append("Dims: ");
			sb.append(mWidth);
			sb.append(",");
			sb.append(mHeight);
			sb.append("\n");
			for(WorldObject d: mObjects) {
				sb.append(d);
				sb.append("\n");
			}
			return sb.toString();
		}
    }